/**
 * 
 */
package cz.enif.console;

/**
 * @author enif
 *
 */
public final class Console 
{
	//// CONSTANTS ////
	
	/** Rychlost blikání kurzoru v milisekundách. */
	private static final int CURSOR_BLINK_INTERVAL = 500;
	
	//// CLASSES ////
	
	/** Znak na obrazovce. */
	private static class ScreenChar
	{
		/** Konstruktor. */
		public ScreenChar()
		{
			this(' ');
		}
		
		/** Konstruktor. */
		public ScreenChar(char c)
		{
			this.c = c;
			this.changed = true;
		}
		
		/** Nastaví znak, ale jen pokud se od posledního nastavení změnil. */
		public void set(char c)
		{
			if (this.c != c)
			{
				this.c = c;
				this.changed = true;
			}
		}
		
		/** Znak. */
		public char c;
		
		/** Příznak, že byl tento znak změněn. */
		public boolean changed;
	}
	
	//// CONSTRUCTORS ////
	
	/** Vytvoří a inicializuje konzoli. */
    public Console(IConsoleRenderer renderer, IKeyboardHandler keyboardHandler, int charsPerLine, int lines)
    {
    	this.useInversedText = false;

    	this.showCursor = true;
    	this.keyboardEcho = true;
    	
    	this.renderer = renderer;
    	this.keyboardHandler = keyboardHandler;
   	
    	this.charsPerLine = charsPerLine;
    	this.lines = lines;
    	
    	this.charBuffer = new ScreenChar[charsPerLine * lines];
    	for (int i = 0; i < this.charBuffer.length; i++)
    	{
    		this.charBuffer[i] = new ScreenChar();
    	}
    	
    	this.clear();
    }
	
	//// PUBLIC ////
	
    /** Vrací počet znaků na řádku. */
    public int getCharsPerLine()
    {
    	return this.charsPerLine;
    }
    
    /** Vrací šířku v pixelech. */
    public int getWidth()
    {
    	return this.charsPerLine * this.renderer.getCharWidth();
    }
    
    /** Vrací počet řádek. */
    public int getLines()
    {
    	return this.lines;
    }
    
    /** Vrací výšku v pixelech. */
    public int getHeight()
    {
    	return this.lines * this.renderer.getCharHeight();
    }
    
    /** Vrací sloupec, ve kterém je kurzor. */
    public int getCursorColumn()
    {
    	return this.cursorColumn;
    }
    
    /** Vrací řádku, na které je kurzor. */
    public int getCursorLine()
    {
    	return this.cursorLine;
    }
    
    
    /** Zapne používání inverzovaného textu. */
    public void setInversedText()
    {
    	this.useInversedText = true;
    	
    	// zresetujeme stav kurzoru
    	this.showCursor(this.showCursor);
    }
    
    /** Vypne používání inverzovaného textu. */
    public void setNormalText()
    {
    	this.useInversedText = false;
    	
    	// zresetujeme stav kurzoru
    	this.showCursor(this.showCursor);
    }
    
    
    /** Vrací true, pokud je kurzor zobrazován. */
    public boolean isCursorVisible()
    {
    	return this.showCursor;
    }
    
    /** Zapne nebo vypne zobrazování kurzoru. */
    public void showCursor(boolean state)
    {
    	this.showCursor = state;
    	
    	// kurzor je inverzní k textu
		this.cursorInverted = !this.useInversedText;
		
		// vynulujeme čítač
		this.lastCursorTime = 0;
    }
    
    /** Zapne nebo vypne echo klávesnice. */
    public void keyboardEcho(boolean state)
    {
    	this.keyboardEcho = state;
    	
    	// pokud se echo zapíná, vymažeme buffer klávesnice
    	if (this.keyboardEcho == true)
    	{
    		this.keyboardHandler.flushCharBuffer();
    	}
    }
    
    /** Vrací počet vykreslených znaků při posledním vykreslení konzole. */
    public int getRenderedCharsCount()
    {
    	return this.renderedCharsCount;
    }
    
    //// FORMATED OUTPUT ////
    
    /** Přesune kurzor na danou pozici.
     * Ořeže pozici tak, aby nebyla mimo obrazovku. */
    public void move(int column, int line)
    {
    	if (column < 0) column = 0;
    	else if (column >= this.charsPerLine) column = this.charsPerLine - 1;
    	this.cursorColumn = column;
 
    	if (line < 0) line = 0;
    	else if (line >= this.lines) line = this.lines - 1;
    	this.cursorLine = line;
    	
    	// zresetujeme stav kurzoru
    	this.showCursor(this.showCursor);
    	
    	this.screenDirty = true;
    }
    
    /** Vypíše na obrazovku řetězec.
	 *
     * Pro vypsání znaku volá putChar(char c). */
    public void putString(String s)
    {
    	// ignorujeme nezadané a prázdné řetězce
    	if (s == null || s.length() == 0) return;
    	
    	for (int i = 0; i < s.length(); i++)
    	{
    		this.putChar(s.charAt(i));
    	}
    }
    
    /** Vypíše znak na aktuální pozici kurzoru a posune jej o znak dále.
     * Po vypsání znaku na konci obrazovky odskroluje text o řádku nahoru.
     * 
     * Podporované speciální znaky: '\r', '\n' */
    public void putChar(char c)
    {
    	if (c == '\r')
		{
			this.cursorColumn = 0;
			
			return;
		}
		else if (c == '\n')
		{
			// přesuneme se na další řádku (kurzor zůstává ve stejném sloupci)
			this.nextLine();
			
			return;
		}
    	
    	// zapíšeme znak
    	this.charBuffer[this.cursorLine * this.charsPerLine + this.cursorColumn].set(c);
   	
    	// posuneme se na další znak
    	this.cursorColumn++;
    	if (this.cursorColumn >= this.charsPerLine)
    	{
    		// přesuneme kurzor na začátek řádky...
    		this.cursorColumn = 0;
    		
    		// a přesuneme se na další řádku
    		this.nextLine();
    	}
    	
    	this.screenDirty = true;
    }
    
    /** Vynutí kompletní překreslení konzole. */
    public void refresh()
    {
    	for (int i = 0; i < this.charBuffer.length; i++)
    	{
    		this.charBuffer[i].changed = true;
    	}
    	
    	this.screenDirty = true;
    } 
    
    /** Smaže konzoli a přesune kurzor na pozici 0, 0. */
    public void clear()
    {
    	for (int i = 0; i < this.charBuffer.length; i++)
    	{
    		this.charBuffer[i].set((char) 0);
    	}
    	
    	this.move(0, 0);
    	
    	this.screenDirty = true;
    }    
    
    //// RAW OUTPUT ////
    
    /** Vloží znak na danou pozici. Nepřesouvá kurzor.
     * Je-li zadaná pozice mimo obrazovku, neprovede nic. */
    public void putCharAt(int column, int line, char c)
    {
    	if ((column < 0 || column >= this.charsPerLine) || (line < 0 || line >= this.lines)) return;
    	
    	this.charBuffer[line * this.charsPerLine + column].set(c);
    }
    
    
    /** Vypíše na danou pozici řetězec.
     * Pro vypsání znaku volá putCharAt(int column, int line, char c). */
    public void putStringAt(int column, int line, String s)
    {
    	// Nevypisujeme prázdný řetězec
    	if (s == null || s.length() == 0) return;
    	
    	for (int i = 0; i < s.length(); i++)
    	{
    		this.putCharAt(column + i, line, s.charAt(i));
    	}
    }
    
    //// EVENTS ////
    
    /** Aktualizuje vnitřní stav konzole.
     * 
     *  Vrací true, pokud se má skončit. */
    public boolean update(long delta)
    {
    	// čteme a zobrazíme vstup z klávesnice
    	if (this.keyboardEcho)
    	{
    		while (this.keyboardHandler.isCharBufferEmpty() == false)
    		{
    			char c = this.keyboardHandler.getChar();
    			
    			if (c < ' ')
    			{
    				switch (c)
    				{
    				case '\n': 
    					// přechod na novou řádku
    					this.putChar('\r');
    					this.putChar('\n');
    					
    					// musíme překreslit obrazovku
    	        		this.screenDirty = true;
    					break;
    					
    				default:
    					; // ignorujeme
    					break;
    				}
    			}
    			else
    			{
    				this.putChar(c);
    			}
    		}
    	}
    	
    	// aktualizace kurzoru
    	if (this.showCursor)
    	{
    		// aktualizace času zobrazení kurzoru
        	this.lastCursorTime += delta;
        	if (this.lastCursorTime >= CURSOR_BLINK_INTERVAL)
        	{
        		// překlopíme inverzi kurzoru
        		this.cursorInverted = !this.cursorInverted;
        		
        		// vynulujeme čítač
        		this.lastCursorTime = 0;
        	}
    	}
    	
    	return false;
    }
    
    //// DRAW ////
    
    /** Vykreslí obsah bufferu konzole na obrazovku. */
    public void draw()
    {
    	this.renderedCharsCount = 0;
    	
    	// pokud se nic na obrazovce nezměnilo, nepřekreslujeme
    	if (this.screenDirty == true)
    	{
    		this.renderer.useInversedFont(this.useInversedText);
        	
        	int bufferIndex = 0; 
        	int charWidth = this.renderer.getCharWidth();
        	int linePos = 0;
        	
        	for (int line = 0; line < this.lines; line++)
        	{
        		int charPos = 0;
        		for (int column = 0; column < this.charsPerLine; column++)
            	{
        			ScreenChar sc = this.charBuffer[bufferIndex++];
        			if (sc.changed)
        			{
        				this.renderer.drawCharAt(charPos, linePos, sc.c);
        				
        				this.renderedCharsCount++;
        				sc.changed = false;
        			}
        			charPos += charWidth;
            	}
        		
        		linePos += this.renderer.getCharHeight();
        	}
    		
    		this.screenDirty = false;
    	}
    	
    	// zobrazení kurzoru jen při změně
    	if (this.showCursor && this.lastCursorTime == 0)
    	{
       		this.renderer.useInversedFont((this.cursorInverted) ? false : true);
       		
       		this.renderer.drawCharAt(
       				this.cursorColumn * this.renderer.getCharWidth(), 
       				this.cursorLine * this.renderer.getCharHeight(), 
       				this.charBuffer[this.cursorLine * this.charsPerLine + this.cursorColumn].c);
    	}
    }
    
    //// KEYBOARD ////
    
    /**
	 * Check if a particular key is pressed
	 * 
	 * @param keyCode The code associated with the key to check
	 * @return True if the specified key is pressed
	 */
	public boolean isKeyPressed(int keyCode) 
    {
		return this.keyboardHandler.isKeyPressed(keyCode);
	}

    /** Nastaví určenou klávesu na "nestisknutou". */
    public void resetKey(int keyCode)
    {
    	this.keyboardHandler.setKeyState(keyCode, false);
    }
    
	//// PRIVATE ////
	
    private boolean useInversedText;
    private boolean showCursor;
    private boolean keyboardEcho;
    
    private final IConsoleRenderer renderer;
    private final IKeyboardHandler keyboardHandler;
    
	private final ScreenChar[] charBuffer;
    private final int charsPerLine;
    private final int lines;
    
    /** Příznak, že se má překreslit obrazovka. */
    private boolean screenDirty;

    /** Počet vykreslených znaků. */
    private int renderedCharsCount;
    
    // pozice kurzoru
    private int cursorColumn; 
    private int cursorLine;
    
    /** Kdy naposledy došlo k aktualizaci kurzoru. */
	private long lastCursorTime;
	private boolean cursorInverted;
    
    
//    /** Smaže obsah řádky od aktuální pozice kurzoru.
//     * Neposouvá kurzor. */
//    private void clearToEol()
//    {
//    	int lineOffset = this.curLine * this.charsPerLine;
//    	for (int i = this.curColumn; i < this.charsPerLine; i++)
//    	{
//    		this.charBuffer[lineOffset + i] = 0;
//    	}
//    	
//    	this.screenDirty = true;
//    }
    
    /** Smaže obsah dané řádky.
     * Neposouvá kurzor. */
    private void clearLine(int line)
    {
    	// nemažeme mimo obrazovku
    	if (line < 0 || line >= this.lines) return;
    	
    	int lineOffset = line * this.charsPerLine;
    	for (int i = 0; i < this.charsPerLine; i++)
    	{
    		this.charBuffer[lineOffset + i].set((char) 0);
    	}
    	
    	this.screenDirty = true;
    }
    
    /** Přesune kurzor na další řádku a odskroluje, je-li třeba. */
    private void nextLine()
    {
    	this.cursorLine++;
		if (this.cursorLine >= this.lines)
		{
			// odskrolujeme o řádku nahoru
			this.scrollUp();
			
			this.cursorLine = this.lines - 1;
		}
    }
    
    /** Posune obsah obrazovky o jednu řádku nahoru. */
    private void scrollUp()
    {
    	for (int i = this.charsPerLine, j = 0; i < this.charBuffer.length; i++, j++)
    	{
			this.charBuffer[j] = this.charBuffer[i];
		}
    	
    	// smažeme poslední řádku (a nastavíme screenDirty na true)
    	this.clearLine(this.lines - 1);
    }
}
